/*
 * JBoss, Home of Professional Open Source
 *
 * Copyright 2013 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gatein.security.oauth.social;

import java.io.IOException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.gatein.common.logging.Logger;
import org.gatein.common.logging.LoggerFactory;
import org.gatein.security.oauth.common.OAuthConstants;
import org.gatein.security.oauth.exception.OAuthException;
import org.gatein.security.oauth.exception.OAuthExceptionCode;
import org.gatein.security.oauth.utils.HttpResponseContext;
import org.gatein.security.oauth.utils.OAuthUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * Processor to perform Facebook interaction
 *
 * @author Anil Saldhana
 * @since Sep 22, 2011
 */
public class FacebookProcessor {

    private static Logger log = LoggerFactory.getLogger(FacebookProcessor.class);

    protected boolean trace = log.isTraceEnabled();


    protected String clientID;
    protected String clientSecret;
    protected String scope;
    protected String returnURL;

    public FacebookProcessor(String clientID, String clientSecret, String scope, String returnURL) {
        super();
        this.clientID = clientID;
        this.clientSecret = clientSecret;
        this.scope = scope;
        this.returnURL = returnURL;
    }


    public boolean initialInteraction(HttpServletRequest request, HttpServletResponse response, String verificationState) throws IOException {
        Map<String, String> params = new HashMap<String, String>();
        params.put(OAuthConstants.REDIRECT_URI_PARAMETER, returnURL);
        params.put(OAuthConstants.CLIENT_ID_PARAMETER, clientID);
        params.put(OAuthConstants.STATE_PARAMETER, verificationState);

        if (scope != null) {
            params.put(OAuthConstants.SCOPE_PARAMETER, scope);
        }

        String location = new StringBuilder(FacebookConstants.SERVICE_URL).append("?").append(OAuthUtils.createQueryString(params))
                .toString();

        if (trace)
            log.trace("Redirect:" + location);
        response.sendRedirect(location);
        return false;
    }

    public String getAccessToken(HttpServletRequest request, HttpServletResponse response) throws OAuthException {
        String authorizationCode = request.getParameter(OAuthConstants.CODE_PARAMETER);
        if (authorizationCode == null) {
            log.error("Authorization code parameter not found");
            handleCodeRequestError(request, response);
            return null;
        }


        String stateFromSession = (String)request.getSession().getAttribute(OAuthConstants.ATTRIBUTE_VERIFICATION_STATE);
        String stateFromRequest = request.getParameter(OAuthConstants.STATE_PARAMETER);
        if (stateFromSession == null || stateFromRequest == null || !stateFromSession.equals(stateFromRequest)) {
            throw new OAuthException(OAuthExceptionCode.INVALID_STATE, "Validation of state parameter failed. stateFromSession="
                    + stateFromSession + ", stateFromRequest=" + stateFromRequest);
        }

        String accessToken = new FacebookRequest<String>() {

            @Override
            protected URL createURL(String authorizationCode) throws IOException {
                return sendAccessTokenRequest(authorizationCode);
            }

            @Override
            protected String parseResponse(String httpResponse) throws JSONException {
                Map<String, String> params = OAuthUtils.formUrlDecode(httpResponse);
                String accessToken = params.get(OAuthConstants.ACCESS_TOKEN_PARAMETER);
                String expires = params.get(FacebookConstants.EXPIRES);
                if (trace)
                    log.trace("Access Token=" + accessToken + " :: Expires=" + expires);

                return accessToken;
            }

        }.executeRequest(authorizationCode);

        return accessToken;
    }

    protected URL sendAccessTokenRequest(String authorizationCode) throws IOException {
        String returnUri = returnURL;

        Map<String, String> params = new HashMap<String, String>();
        params.put(OAuthConstants.REDIRECT_URI_PARAMETER, returnUri);
        params.put(OAuthConstants.CLIENT_ID_PARAMETER, clientID);
        params.put(OAuthConstants.CLIENT_SECRET_PARAMETER, clientSecret);
        params.put(OAuthConstants.CODE_PARAMETER, authorizationCode);

        String location = new StringBuilder(FacebookConstants.ACCESS_TOKEN_ENDPOINT_URL).append("?")
                .append(OAuthUtils.createQueryString(params)).toString();

        if (trace)
            log.trace("AccessToken Request=" + location);
        return new URL(location);
    }

    public Set<String> getScopes(String accessToken) {
        Set<String> scopes = new FacebookRequest<Set<String>>() {

            @Override
            protected URL createURL(String accessToken) throws IOException {
                String urlString = new StringBuilder(FacebookConstants.PROFILE_ENDPOINT_URL).append("/permissions").append("?access_token=")
                        .append(URLEncoder.encode(accessToken, "UTF-8")).toString();
                if (trace)
                    log.trace("Read info about available scopes:" + urlString);

                return new URL(urlString);
            }

            @Override
            protected Set<String> parseResponse(String httpResponse) throws JSONException {
                JSONObject jsonObject = new JSONObject(httpResponse);

                JSONArray json = jsonObject.getJSONArray("data");
                if (json != null) {
                    jsonObject = json.optJSONObject(0);
                    if (jsonObject != null) {
                        String[] names = JSONObject.getNames(jsonObject);
                        if (names != null) {
                            Set<String> scopes = new HashSet<String>();
                            for (String name : names) {
                                scopes.add(name);
                            }
                            return scopes;
                        }
                    }
                }

                return new HashSet<String>();
            }

        }.executeRequest(accessToken);

        return scopes;
    }

    public FacebookPrincipal getPrincipal(String accessToken) {
        FacebookPrincipal facebookPrincipal = new FacebookRequest<FacebookPrincipal>() {

            private String accessToken;

            @Override
            protected URL createURL(String accessToken) throws IOException {
                String urlString = new StringBuilder(FacebookConstants.PROFILE_ENDPOINT_URL).append("?access_token=")
                        .append(URLEncoder.encode(accessToken, "UTF-8")).toString();
                if (trace)
                    log.trace("Profile read:" + urlString);

                // Little hack but sufficient for now
                this.accessToken = accessToken;

                return new URL(urlString);
            }

            @Override
            protected FacebookPrincipal parseResponse(String httpResponse) throws JSONException {
                JSONObject jsonObject = new JSONObject(httpResponse);

                FacebookPrincipal facebookPrincipal = new FacebookPrincipal();
                facebookPrincipal.setAccessToken(accessToken);
                facebookPrincipal.setId(jsonObject.getString("id"));
                facebookPrincipal.setName(jsonObject.optString("name"));
                facebookPrincipal.setUsername(jsonObject.optString("username"));
                facebookPrincipal.setFirstName(jsonObject.optString("first_name"));
                facebookPrincipal.setLastName(jsonObject.optString("last_name"));
                facebookPrincipal.setGender(jsonObject.optString("gender"));
                facebookPrincipal.setTimezone(jsonObject.optString("timezone"));
                facebookPrincipal.setLocale(jsonObject.optString("locale"));
                facebookPrincipal.setEmail(jsonObject.optString("email"));
                facebookPrincipal.setJsonObject(jsonObject);

                // This could happen with Facebook testing users
                if (facebookPrincipal.getUsername() == null || facebookPrincipal.getUsername().length() == 0) {
                    facebookPrincipal.setUsername(facebookPrincipal.getId());
                }

                return facebookPrincipal;
            }

        }.executeRequest(accessToken);

        return facebookPrincipal;
    }

    public void revokeToken(String accessToken) {
        try {
            String urlString = new StringBuilder(FacebookConstants.PROFILE_ENDPOINT_URL).append("/permissions?access_token=")
                    .append(URLEncoder.encode(accessToken, "UTF-8")).append("&method=delete").toString();
            URL revokeUrl = new URL(urlString);
            HttpResponseContext revokeContent = OAuthUtils.readUrlContent(revokeUrl.openConnection());
            if (revokeContent.getResponseCode() != 200) {
                throw new OAuthException(OAuthExceptionCode.TOKEN_REVOCATION_FAILED,
                        "Error when revoking token. Http response code: " + revokeContent.getResponseCode() + ", Error details: " + revokeContent.getResponse());
            }

            if (log.isTraceEnabled()) {
                log.trace("Successfully revoked facebook accessToken " + accessToken + ", revokeContent=" + revokeContent);
            }
        } catch (IOException ioe) {
            throw new OAuthException(OAuthExceptionCode.TOKEN_REVOCATION_FAILED, "Error when revoking token", ioe);
        }
    }

    private void handleCodeRequestError(HttpServletRequest request, HttpServletResponse response) {
        // Log all possible error parameters
        StringBuilder errorBuilder = new StringBuilder();
        Enumeration<String> paramNames = request.getParameterNames();
        while (paramNames.hasMoreElements()) {
            String paramName = paramNames.nextElement();
            if (paramName.startsWith("error")) {
                errorBuilder.append(paramName + ": " + request.getParameter(paramName) + System.getProperty("line.separator"));
            }
        }
        String errorMessage = errorBuilder.toString();

        String error = request.getParameter(OAuthConstants.ERROR_PARAMETER);
        if (error != null) {
            if (OAuthConstants.ERROR_ACCESS_DENIED.equals(error)) {
                throw new OAuthException(OAuthExceptionCode.USER_DENIED_SCOPE, errorMessage);
            }
        }

        throw new OAuthException(OAuthExceptionCode.FACEBOOK_ERROR, errorMessage);
    }

}

